package com.huanzhidadi.jsoup.util;

import com.alibaba.fastjson2.JSON;
import com.huanzhidadi.jsoup.annotation.Extract;
import com.huanzhidadi.jsoup.annotation.JsoupDocument;
import com.huanzhidadi.jsoup.entity.Article;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.internal.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

public class CrawlerUtil {

    /**
     * 获取connection
     * @param url
     * @return
     */
    public static Connection getConnection(String url) {
        Connection connection = Jsoup.connect(url);
        // 3、伪造请求头
        connection.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
        connection.header("Accept-Encoding", "gzip, deflate");
        connection.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
        connection.header("Cache-Control", "max-age=0");
        connection.header("Connection", "keep-alive");
        connection.header("Cookie", "csrf_token=b583f64d00c0422f821f6a3ad61816ab; __tins__19731447=%7B%22sid%22%3A%201691396418061%2C%20%22vd%22%3A%201%2C%20%22expires%22%3A%201691398218061%7D; __51cke__=; __51laig__=1");
        connection.header("Host", "blog.huanzhidadi.com");
        connection.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188");
        connection.ignoreHttpErrors(true);
        return connection;
    }

    public static Object execute(Class<?> clazz, String url, Object obj) throws Exception {
        // 获取JsoupDocument
        JsoupDocument jsoupDocument = clazz.getAnnotation(JsoupDocument.class);
        // 判断注解不为空
        if (Objects.isNull(jsoupDocument)) {
            throw new RuntimeException("该实体类不是爬虫实体。");
        }
        String domain = jsoupDocument.domain();
        String targetUrl = jsoupDocument.targetUrl();
        // 如果存在targetUrl，那么爬取的目标url以targetUrl为主
        if (!StringUtil.isBlank(targetUrl)) {
            url = targetUrl;
        }
        // 判断url是否为空
        if (StringUtil.isBlank(url)) {
            throw new RuntimeException("未指定爬取目标url。");
        }
        Connection connection = getConnection(url);
        Connection.Method method = jsoupDocument.method();
        // 获取document
        Document document = connection.method(method).execute().parse();
        Element bodyElement = document.body();
        // 获取类上的css选择器
        String cssQuery = jsoupDocument.cssQuery();
        // 获取该类所有字段
        Field[] declaredFields = clazz.getDeclaredFields();

        // 判断返回对象的类型
        if (obj instanceof List) {
            // 是集合
            // 获取目标列表
            Elements entityElements = bodyElement.select(cssQuery);
            return handlerListValue(clazz, (List) obj, entityElements, declaredFields);
        } else {
            // 是实体类
            return handlerPojoValue(obj, bodyElement, declaredFields, cssQuery);
        }
    }

    /**
     * 处理实体类
     * @param obj
     * @param bodyElement
     * @param declaredFields
     * @param cssQuery
     * @return
     * @throws IllegalAccessException
     */
    private static Object handlerPojoValue(Object obj, Element bodyElement, Field[] declaredFields, String cssQuery) throws Exception {
        if (StringUtil.isBlank(cssQuery)) {
            setFieldValue(obj, declaredFields, bodyElement);
        } else {
            Elements entityElements = bodyElement.select(cssQuery);
            int size = entityElements.size();
            if (0 == size) {
                return obj;
            }
            Element element = entityElements.get(0);
            setFieldValue(obj, declaredFields, element);
        }
        return obj;
    }

    /**
     * 处理集合
     * @param clazz
     * @param obj
     * @param entityElements
     * @param declaredFields
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    private static List handlerListValue(Class<?> clazz, List obj, Elements entityElements, Field[] declaredFields) throws Exception {
        List list = obj;
        for (Element entityElement : entityElements) {
            // 创建对象
            Object instance = clazz.newInstance();
            setFieldValue(instance, declaredFields, entityElement);
            list.add(instance);
        }
        return list;
    }

    private static void setFieldValue(Object obj, Field[] declaredFields, Element entityElement) throws Exception {
        for (Field field : declaredFields) {
            field.setAccessible(true);

            // 获取JsoupDocument注解
            JsoupDocument fieldJsoupDocument = field.getAnnotation(JsoupDocument.class);
            if (null != fieldJsoupDocument) {
                // 递归爬取
                Class<?> fieldType = field.getType();
                Object fieldObject = fieldType.newInstance();
                // 获取url
                String hrefAttr = fieldJsoupDocument.hrefAttr();
                if (StringUtil.isBlank(hrefAttr)) {
                    throw new RuntimeException("字段" + field.getName() + "没有指定从哪个字段中获取目标url");
                }
                // 获取指定字段
                Class<?> objClass = obj.getClass();
                Field hrefField = objClass.getDeclaredField(hrefAttr);
                hrefField.setAccessible(true);
                // 获取该字段的值
                String hrefValue = (String) hrefField.get(obj);
                if (StringUtil.isBlank(hrefValue)) {
                    continue;
                }
                // 处理域名
                if (!hrefValue.toLowerCase().startsWith("http")) {
                    String domain = fieldJsoupDocument.domain();
                    hrefValue = domain + hrefValue;
                }

                Object childValue = execute(fieldType, hrefValue, fieldObject);
                // 设置值
                field.set(obj, childValue);
                continue;
            }

            // 获取Extract注解
            Extract extract = field.getAnnotation(Extract.class);
            if (extract != null) {
                // 不为空，需要抽取数据
                // 获取css选择器
                String fieldQuery = extract.cssQuery();
                // 抽取数据
                Elements fieldElement = entityElement.select(fieldQuery);

                // 判断该字段的值是否要从属性中获取
                String attr = extract.attr();
                if (!StringUtil.isBlank(attr)) {
                    // 获取属性值，赋值给字段
                    String attrValue = fieldElement.attr(attr);
                    field.set(obj, attrValue);
                } else {
                    // attr为空，获取contentType，觉得如何取值
                    int contentType = extract.contentType();
                    if (1 == contentType) {
                        // 执行text
                        String text = fieldElement.text();
                        // 处理前缀
                        String prefix = extract.prefix();
                        if (!StringUtil.isBlank(prefix)) {
                            text = text.replaceFirst(prefix, "");
                        }
                        // 赋值给字段
                        field.set(obj, text);
                    } else if (2 == contentType) {
                        String html = fieldElement.html();
                        field.set(obj, html);
                    } else {
                        // 存在了1和2以外的值，抛出异常
                        throw new RuntimeException("字段" + field.getName() + "抽取类型只能是1或2");
                    }
                }

            }
        }
    }

    public static void main(String[] args) throws Exception {
        Article article = (Article) CrawlerUtil.execute(Article.class, "", new Article());
        System.out.println(JSON.toJSONString(article));

//        List<Article> articleList = (List<Article>) CrawlerUtil.execute(Article.class, "", new ArrayList<Article>());
//        System.out.println(JSON.toJSONString(articleList));
    }
}
